iT邦幫忙

2021 iThome 鐵人賽

DAY 2
0
自我挑戰組

每日一杯 JavaScript 特調系列 第 2

執行環境 Execution Context、宣告提升 Hoisting

  • 分享至 

  • xImage
  •  

早期的 JavaScript 變數只能用 var 宣告,後來 ES6 新增 let 、 const 。
這篇不會細講三個宣告方式的差異,網路上很多大神已經解釋得很好。另外,其實我現在已經沒在用 var ,但用 var 才能解釋宣告提升這個行為。

那麼進入正題,宣告提升 ( Hoisting ) 到底是什麼樣的行為?他們怎麼在 JS 中被解析的?

我認為從 JS 的 執行環境 ( execution context ) 機制說起更好理解:

JS 的運作機制是一行一行往下執行的單執行緒,每當解析器碰到一個函式被呼叫, 就會為該函式開啟一個 execution context ,我們常稱中文為執行環境。

但要記得,第一個執行環境皆是全域執行環境 ( Global Execution Context )會被創建,且即便你有多個 JS 檔,他們共用同個全域執行環境。我個人是會把網站的所有 JS 檔案,想成被包進同個函式中,當你開啟網站頁面時就是呼叫這個函式。
就像不同的火鍋料丟進同個火鍋裡,但這些料都是同個湯底(?
https://ithelp.ithome.com.tw/upload/images/20210914/20141763i4QhJoL0Zd.png

這會影響到,你在不同檔案的最外層(也就是全域環境)取了同名稱變數,解析器就會報錯,因為他們是共用同個全域執行環境。

爾後,其他函式被呼叫時都是一個個堆疊於 Global Execution Context 之上。

好咧!接著要細講 Execution Context 的機制

Execution Context 執行環境

Execution Context 整個過程分為兩個階段 - 創建階段、執行階段,且 Execution Context 還分成全域執行環境以及函式執行環境。

第一階段:創建階段 Creation Phase

  • 若是全域執行環境,會建立 global object

  • 皆會創建 this ,而 全域的 this 就是 window 物件,函式的 this 則會依據多種情況做綁定

  • 每個執行環境,都會將宣告的變數名稱以及函式放入記憶體的一個專屬空間中
    ( ! ) 這邊有個大坑,以下面例子說明

    console.log(idol);
    sing('I');
    var idol = 'Taeyeon';
    function sing(song){ console.log(song)};
    console.log(idol);
    

在創建階段,記憶體僅會放入的是變數 idol 這個名稱以及 sing 函式的全部程式碼,就是所謂的「宣告提升 Hoisting」。

https://ithelp.ithome.com.tw/upload/images/20210914/201417638dInEzzgUq.png

但 console.log(idol) 為什麼是 undefined?因為 JS 第一階段就會把所有變數名稱先寫在記憶體中,此時它會給所有變數賦予 undefined,直到第二階段開始逐行執行時,有賦值才給值,這也是為什麼第五行 console.log(idol); 就能得到我們想要的值,因為我們在第三行賦值了。

but !! sing('I')為什麼可以成功執行?因為 JS 在創建階段對於函式的宣告,會把整個函式包含程式碼( Function Statement )都放進記憶體。

這是一般變數跟函式在宣告提升上的差別!變數的宣告提升是不包含賦值這件事。

再看一個例子:

```javascript
cheer(idol); // error
var idol = 'Taeyeon';
var cheer = function(idol){ 
  console.log(`cheer for ${idol}`)
};
cheer(idol); // cheer for Taeyeon
```

跟之前不同的是,cheer 我們用運算子的方式去賦值一個匿名函式,而非直接宣告成函式。這會導致放進整個記憶體的依然僅有 cheer 變數名稱,不包含匿名函式的程式碼,因此第一行呼叫 cheer(idol) 會報錯,系統沒辦法知道 cheer 是一個 function。

第二階段:執行階段 Execution Phase

當創建階段完成後,就會進入到執行階段,也就是逐行 run 程式碼的時候
執行階段的核心觀念在執行堆疊 Execution Stack ( 也稱為 Call Stack )
Stack 是一種資料結構,特色為後進先出,也就是最後進來的會優先執行

例子講解

我們現在有一個 concert 檔,從右邊可以看到我們演唱會表演嘉賓的值以及他即將做哪些表演。當 concert 執行時,左邊最下層會先創建一個 global execution context,裡面儲存所有變數名稱跟宣告的函式,創建階段結束後開始逐行跑右邊程式碼。

https://ithelp.ithome.com.tw/upload/images/20210914/20141763zXdMnymVLr.png

執行階段:

  1. 首先變數 idol 得到值 Taeyeon
  2. 接下來兩個都是函式宣告,不用執行
  3. 呼叫 sing() 函式,於是創建一個屬於 sing()的「新 execution context」,並堆疊在 global 之上
  4. 開始執行 sing 的程式碼,遇到呼叫 dance() 函式,又創建屬於 dance()的「新 execution context」,並堆疊在 sing 的 context 之上
  5. 當 dance() 執行完後,dance 的 context 會跳出 ( pop off )整個 execution stack,並回到 sing()
  6. 當 sing() 執行完後同樣也會跳出 stack,最終回到 global execution context 繼續執行下面的 code ( concert 例子到 sing() 就結束了,但正常來講一份 js 還會有很多要執行的東西 )

以上統整幾個重點

  • 全域執行環境會先被創建

  • 宣告提升發生在執行環境的創建階段 → 這邊要注意全域跟函式的執行環境都有創建階段,如果你在函式內宣告變數,所有行為都跟文章第一部分講的一樣,會在該函式的作用域內進行 Hoisting喔!

    var idol = 'Taeyeon';
    function sing(singer){
      console.log(idol) // undefined  因為下一行 var idol; 已經被放進記憶體內 但還沒執行到賦值的動作
                        // 即便外層全域也有 idol,但被函式 exection context 裡的同名稱 idol 變數給取代
      var idol = 'Key'
      console.log(idol) // "Fine" 上一行賦值後就能成功取得值
    }
    
    sing(idol);
    
  • JS 的單執行緒機制讓程式同個時間只能做一件事,做完才接下一個

  • JS 的執行 stack 採後進先出

  • 每個執行環境 execution context 執行完後會跳離 stack,如下圖紅箭頭,直到回到最底部的全域執行環境

https://ithelp.ithome.com.tw/upload/images/20210914/2014176378qGkaznkI.png

最後稍微提一下「 暫時性死區 ( temporal dead zone,簡稱TDZ )

為什麼開頭會說必須用 var 才能說明 hoisting呢?

這是因為 let 跟 const 的宣告提升會被 TDZ 蓋過去,TDZ 是一個時間點的概念,並非空間。

就我的認知上,我不覺得 let、const 不具有 hoisting 的效果,而是他們宣告提升後被自身特性給限制住。倘若已經宣告但尚未賦值,JS 也不會主動把它設為 undefined (更不用說 const 規定宣告時一定要有值 ),所以想取值就會直接報錯,錯誤好像會因瀏覽器不同而定,我比較常見的是 "ReferenceError: Cannot access 'idol' before initialization

console.log(idol); // 受 TDZ 影響 會報錯
let idol = 'Taegu';
let idol2;
console.log(idol2) // undefined

不過這件事有多派理解,大家就記得 let 、 const 在賦值前取用的話會報錯,而不是 undefined。

一起養成先宣告好再取用變數的習慣~


參考資料

Huli 大神 https://blog.techbridge.cc/2018/11/10/javascript-hoisting/
https://medium.com/itsems-frontend/javascript-execution-context-and-call-stack-e36e7f77152e
Udemy 課程


上一篇
JavaScript 的資料型別 (data type) 及存取值
下一篇
同步、非同步事件控制
系列文
每日一杯 JavaScript 特調7
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言